/*
 * NBT.c : Parse a NBT (Named Binary Tag) file format, supports Alpha & Beta level format.
 *
 * Written by T.Pierron, Oct 2010
 */

#include <stdio.h>
#include <zlib.h>
#include <stdlib.h>
#include <string.h>
#include "NBT.h"


static struct Item_t items[] = {
	{1, "Stone"},
	{2, "Grass"},
	{3, "Dirt"},
	{4, "Cobblestone"},
	{5, "Wooden Plank"},
	{6, "Sapling"},
	{7, "Bedrock"},
	{8, "Water"},
	{9, "Stationary water"},
	{10, "Lava"},
	{11, "Stationary lava"},
	{12, "Sand"},
	{13, "Gravel"},
	{14, "Gold ore"},
	{15, "Iron ore"},
	{16, "Coal ore"},
	{17, "Wood"},
	{18, "Leaves"},
	{19, "Sponge"},
	{20, "Glass"},
	{21, "Lapis Lazuli Ore"},
	{22, "Lapis Lazuli Block"},
	{23, "Dispenser"},
	{24, "Sandstone"},
	{25, "Note Block"},
	{26, "Bed"},
	{27, "Powered Rail"},
	{28, "Detector Rail"},
	{30, "Web"},
	{35, "Wool"},
	{37, "Yellow flower"},
	{38, "Red rose"},
	{39, "Brown Mushroom"},
	{40, "Red Mushroom"},
	{41, "Gold Block"},
	{42, "Iron Block"},
	{43, "Double Stone Slab"},
	{44, "Stone Slab"},
	{45, "Brick Block"},
	{46, "TNT"},
	{47, "Bookshelf"},
	{48, "Moss Stone"},
	{49, "Obsidian"},
	{50, "Torch"},
	{51, "Fire"},
	{52, "Monster Spawner"},
	{53, "Wooden Stairs"},
	{54, "Chest"},
	{55, "Redstone Wire"},
	{56, "Diamond Ore"},
	{57, "Diamond Block"},
	{58, "Workbench"},
	{59, "Crops"},
	{60, "Farmland"},
	{61, "Furnace"},
	{62, "Burning Furnace"},
	{63, "Sign Post"},
	{64, "Wooden Door"},
	{65, "Ladder"},
	{66, "Minecart Tracks"},
	{67, "Cobblestone Stairs"},
	{68, "Wall Sign"},
	{69, "Lever"},
	{70, "Stone Pressure Plate"},
	{71, "Iron Door"},
	{72, "Wooden Pressure Plate"},
	{73, "Redstone Ore"},
	{74, "Glowing Redstone Ore"},
	{75, "Redstone torch (\"off\" state)"},
	{76, "Redstone torch (\"on\" state)"},
	{77, "Stone Button"},
	{78, "Snow"},
	{79, "Ice"},
	{80, "Snow Block"},
	{81, "Cactus"},
	{82, "Clay"},
	{83, "Sugar Cane"},
	{84, "Jukebox"},
	{85, "Fence"},
	{86, "Pumpkin"},
	{87, "Netherrack"},
	{88, "Soul Sand"},
	{89, "Glowstone"},
	{90, "Portal"},
	{91, "Jack-O-Lantern"},
	{92, "Cake Block"},
	{93, "Redstone Repeater (\"off\" state)"},
	{94, "Redstone Repeater (\"on\" state)"},
	{256, "Iron Shovel"},
	{257, "Iron Pickaxe"},
	{259, "Flint and Steel"},
	{260, "Apple"},
	{261, "Bow"},
	{262, "Arrow"},
	{263, "Coal"},
	{264, "Diamond"},
	{265, "Iron Ingot"},
	{266, "Gold Ingot"},
	{267, "Iron Sword"},
	{268, "Wooden Sword"},
	{269, "Wooden Shovel"},
	{270, "Wooden Pickaxe"},
	{271, "Wooden Axe"},
	{272, "Stone Sword"},
	{273, "Stone Shovel"},
	{274, "Stone Pickaxe"},
	{275, "Stone Axe"},
	{276, "Diamond Sword"},
	{277, "Diamond Shovel"},
	{278, "Diamond Pickaxe"},
	{279, "Diamond Axe"},
	{280, "Stick"},
	{281, "Bowl"},
	{282, "Mushroom Soup"},
	{283, "Gold Sword"},
	{284, "Gold Shovel"},
	{285, "Gold Pickaxe"},
	{286, "Gold Axe"},
	{287, "String"},
	{288, "Feather"},
	{289, "Sulphur"},
	{290, "Wooden Hoe"},
	{291, "Stone Hoe"},
	{292, "Iron Hoe"},
	{293, "Diamond Hoe"},
	{294, "Gold Hoe"},
	{295, "Seeds"},
	{296, "Wheat"},
	{297, "Bread"},
	{298, "Leather Helmet"},
	{299, "Leather Chestplate"},
	{300, "Leather Leggings"},
	{301, "Leather Boots"},
	{302, "Chainmail Helmet"},
	{303, "Chainmail Chestplate"},
	{304, "Chainmail Leggings"},
	{305, "Chainmail Boots"},
	{306, "Iron Helmet"},
	{307, "Iron Chestplate"},
	{308, "Iron Leggings"},
	{309, "Iron Boots"},
	{310, "Diamond Helmet"},
	{311, "Diamond Chestplate"},
	{312, "Diamond Leggings"},
	{313, "Diamond Boots"},
	{314, "Gold Helmet"},
	{315, "Gold Chestplate"},
	{316, "Gold Leggings"},
	{317, "Gold Boots"},
	{318, "Flint"},
	{319, "Raw Porkchop"},
	{320, "Cooked Porkchop"},
	{321, "Paintings"},
	{322, "Golden apple"},
	{323, "Sign"},
	{324, "Wooden door"},
	{325, "Bucket"},
	{326, "Water bucket"},
	{327, "Lava bucket"},
	{328, "Minecart"},
	{329, "Saddle"},
	{330, "Iron door"},
	{331, "Redstone"},
	{332, "Snowball"},
	{333, "Boat"},
	{334, "Leather"},
	{335, "Milk"},
	{336, "Clay Brick"},
	{337, "Clay Balls"},
	{338, "Sugar Cane"},
	{339, "Paper"},
	{340, "Book"},
	{341, "Slimeball"},
	{342, "Storage Minecart"},
	{343, "Powered Minecart"},
	{344, "Egg"},
	{345, "Compass"},
	{346, "Fishing Rod"},
	{347, "Clock"},
	{348, "Glowstone Dust"},
	{349, "Raw Fish"},
	{350, "Cooked Fish"},
	{351, "Ink Sac"},
	{352, "Bone"},
	{353, "Sugar"},
	{354, "Cake"},
	{355, "Bed"},
	{356, "Redstone Repeater"},
	{357, "Cookie"},
	{2256, "Gold Music Disc"},
	{2257, "Green Music Disc"}
};

static ZStream gzOpen(STRPTR path, int type, int offset)
{
	ZStream io = malloc(sizeof *io + (type == 1 ? SZ_CHUNK : 0));

	if (io)
	{
		uint8_t header[5];
		memset(io, 0, sizeof *io);
		io->type = type;
		switch (type) {
		case 0:
			io->gzin = gzopen(path, "rb");
			if (io->gzin == NULL) { free(io); return NULL; }
			break;
		case 1:
			io->in = fopen(path, "rb");
			if (io->in)
			{
				fseek(io->in, offset, SEEK_SET);
				fread(header, 1, sizeof header, io->in);
				if (header[4] == 2)
				{
					io->bout = (APTR) (io + 1);
					io->bin = io->bout + SZ_CHUNK/2;
					io->remain = (((((header[0] << 8) | header[1]) << 8) | header[2]) << 8) | header[3];
					io->strm.next_out = io->bout;
					io->strm.avail_out = SZ_CHUNK/2;
					io->strm.next_in = io->bin;
					if (inflateInit(&io->strm) == Z_OK)
						return io;
				}
				fclose(io->in);
			}
			free(io);
			return NULL;
		}
	}
	return io;
}

static int gzRead(ZStream in, APTR buffer, int len)
{
	int left, ret = 0;
	switch (in->type) {
	case 0: return gzread(in->gzin, buffer, len);
	case 1:
		while (len > 0)
		{
			left = in->strm.next_out - (in->bout + in->read);
			if (left > 0)
			{
				if (left > len) left = len;
				memcpy(buffer, in->bout + in->read, left);
				buffer += left; len -= left;
				in->read += left;
				ret += left;
				if (len == 0) return ret;
			}
			/* Caller want more stuff, but we are out of data */
			in->strm.next_out = in->bout;
			in->strm.avail_out = SZ_CHUNK/2;
			in->read = 0;
			if (in->strm.avail_in < SZ_CHUNK/4 && in->remain) {
				left = SZ_CHUNK/2 - in->strm.avail_in;
				if (left > in->remain) left = in->remain;
				if (left > 0) {
					memmove(in->bin, in->strm.next_in, in->strm.avail_in);
					in->strm.next_in = in->bin;
					left = fread(in->bin + in->strm.avail_in, 1, left, in->in);
					in->strm.avail_in += left;
					in->remain -= left;
				}
			}
			if (inflate(&in->strm, 0) == Z_DATA_ERROR)
				return -1;
		}
	}
	return ret;
}

static int gzGetC(ZStream in)
{
	uint8_t c;
	if (gzRead(in, &c, 1) == 1) return c;
	return -1;
}

static void gzClose(ZStream in)
{
	switch (in->type) {
	case 0: gzclose(in->gzin); break;
	case 1: inflateEnd(&in->strm); fclose(in->in);
	}
	free(in);
}

static void swapbytes(APTR data, int len)
{
	STRPTR s = data;
	STRPTR e = s + len - 1;

	while (s < e)
	{
		swap(*s, *e);
		s ++; e --;
	}
}

static int sizeof_type[] = {0, 1, 2, 4, 8, 4, 8};

static NBT NBT_ParseFile(ZStream in)
{
	int len, type, i, n;
	NBT node, sub;

	/* First byte: node type */
	type = gzGetC(in);
	if (type == 0) return NULL;

	/* Get node name */
	len = WORD(in);
	node = calloc(sizeof *node + len + 1, 1);
	node->name = (STRPTR) (node+1);
	node->type = type;
	gzRead(in, node->name, len);
	node->name[len] = 0;

	switch (node->type) {
	case 1: node->val.byte  = gzGetC(in); len = 1; break; // TAG_Byte
	case 2: node->val.word  = WORD(in);   len = 2; break; // TAG_Short
	case 3: node->val.dword = DWORD(in);  len = 4; break; // TAG_Int
	case 4: // TAG_Long
		for (i = 0; i < 8; i ++) {
			node->val.qword <<= 8;
			node->val.qword |= (uint8_t) gzGetC(in);
		}
		len = 8;
		break;
	case 5: gzRead(in, &node->val.real32, len = 4); swapbytes(&node->val.real32, 4); break; // TAG_Float
	case 6: gzRead(in, &node->val.real64, len = 8); swapbytes(&node->val.real32, 8); break; // TAG_Double
	case 7: // TAG_Byte_Array
		len = DWORD(in);
		node->val.array = malloc(len);
		node->len = len;
		gzRead(in, node->val.array, len);
		break;
	case 8: // TAG_String
		node->len = len = WORD(in);
		node->val.string = malloc(len+1);
		gzRead(in, node->val.string, len);
		node->val.string[len] = 0;
		break;
	case 9: // TAG_List
		type = gzGetC(in);
		len  = DWORD(in);
		node->type |= type << 8;
		switch (type) {
		case 1:
		case 2:
		case 3:
		case 4:
		case 5:
		case 6:
			// Can be parsed directly
			node->len = len;
			len *= sizeof_type[type];
			node->val.string = malloc(len);
			gzRead(in, node->val.string, len);
			break;
		case 7: // TAG_Byte_Array
		case 9: // TAG_List
			fprintf(stderr, "unsupported nested list. Exiting.\n");
			break;
		case 8: // TAG_String
			for (n = i = 0; i < len; i ++)
			{
				int lg = WORD(in);
				node->val.string = realloc(node->val.string, n + lg + 1);
				gzRead(in, node->val.string + n, lg);
				node->val.string[n+lg] = 0;
				n += lg + 1;
			}
			break;
			break;
		case 10: // TAG_Compound
			node->val.array = calloc(sizeof (APTR), len);
			node->len = len;
			for (i = 0; i < len; i ++)
			{
				NBT root = node->val.array[i] = calloc(sizeof *node, 1);
				root->type = 10;
				root->name = "";
				while ((sub = NBT_ParseFile(in)))
				{
					ListAddTail(&root->child, &sub->node);
					sub->parent = root;
				}
			}
		}
		break;
	case 10: // TAG_Compound
		while ((sub = NBT_ParseFile(in)))
		{
			ListAddTail(&node->child, &sub->node);
			sub->parent = node;
		}
		break;
	default:
		return NULL;
	}
	return node;
}

void NBT_Free(NBT root)
{
	NBT child, next;
	int i;

	if (root == NULL) return;

	for (child = next = HEAD(root->child); child; child = next)
	{
		NEXT(next);
		NBT_Free(child);
	}
	switch (root->type & 0xff) {
	case TAG_Byte_Array: case TAG_String:
		if (root->val.string) free(root->val.string);
		break;
	case TAG_List:
		if (root->val.array)
		{
			if (root->type == TAG_List_Compound)
				for (i = 0; i < root->len; i ++)
					NBT_Free(root->val.array[i]);
			free(root->val.array);
		}
	}
	free(root);
}

/* Find node within using breadth-first exploration */
NBT NBT_FindNode(NBT root, STRPTR name)
{
	if (root == NULL) return NULL;
	if (strcasecmp(root->name, name) == 0) return root;
	NBT sub, find;
	int i;

	switch (root->type) {
	case TAG_Compound:
		for (sub = HEAD(root->child); sub; NEXT(sub))
			if (strcasecmp(sub->name, name) == 0) return sub;

		for (sub = HEAD(root->child); sub; NEXT(sub))
			if ((sub->type & 0xff) >= TAG_List && (find = NBT_FindNode(sub, name))) return find;

		break;

	case TAG_List_Compound:
		for (i = 0; i < root->len; i ++)
		{
			sub = root->val.array[i];
			if (strcasecmp(sub->name, name) == 0) return sub;
		}
		for (i = 0; i < root->len; i ++)
		{
			sub = root->val.array[i];
			if ((sub->type & 0xff) >= TAG_List && (find = NBT_FindNode(sub, name))) return find;
		}
	}
	return NULL;
}

Bool NBT_ConvertToInt(NBT node, int * array, int nb)
{
	int i, type = node->type >> 8;
	if (nb > node->len || (node->type & 0xff) != 9) return False;

	for (i = 0; i < nb; i ++)
	{
		TEXT buffer[8];
		int  sz = sizeof_type[type];

		memcpy(buffer, node->val.string + i * sz, sz);
		swapbytes(buffer, sz);
		switch (type) {
		case 1: array[i] = * (int8_t  *) buffer; break;
		case 2: array[i] = * (int16_t *) buffer; break;
		case 3: array[i] = * (int32_t *) buffer; break;
		case 4: array[i] = * (int64_t *) buffer; break;
		case 5: array[i] = * (float   *) buffer; break;
		case 6: array[i] = * (double  *) buffer;
		}
	}
	return True;
}

STRPTR NBT_ToString(NBT node, STRPTR id, STRPTR def)
{
	if ((node = NBT_FindNode(node, id)))
	{
		static TEXT number[32];
		switch (node->type & 0xff) {
		case 1: sprintf(def = number, "%d", node->val.byte); break;
		case 2: sprintf(def = number, "%d", node->val.word); break;
		case 3: sprintf(def = number, "%d", node->val.dword); break;
		case 4: sprintf(def = number, "%d", node->val.byte); break;
		case 5: sprintf(def = number, "%g", node->val.real32); break;
		case 6: sprintf(def = number, "%g", node->val.real64); break;
		case 8: return node->val.string;
		}
	}
	return def;
}

int NBT_ToInt(NBT node, STRPTR id, int def)
{
	if ((node = NBT_FindNode(node, id)))
	{
		switch (node->type & 0xff) {
		case 1: return node->val.byte;
		case 2: return node->val.word;
		case 3: return node->val.dword;
		case 4: return node->val.byte;
		case 5: return node->val.real32;
		case 6: return node->val.real64;
		case 8: return strtol(node->val.string, NULL, 10);
		}
	}
	return def;
}

Bool NBT_SetArrayValue(NBT node, int i, int val)
{
	if (i >= node->len || (node->type & 0xff) != 9) return False;

	TEXT buffer[8];
	int  type = node->type >> 8;
	int  sz = sizeof_type[type];

	switch (type) {
	case 1: * (int8_t  *) buffer = val; break;
	case 2: * (int16_t *) buffer = val; break;
	case 3: * (int32_t *) buffer = val; break;
	case 4: * (int64_t *) buffer = val; break;
	case 5: * (float   *) buffer = val; break;
	case 6: * (double  *) buffer = val;
	}
	swapbytes(buffer, sz);
	memcpy(node->val.string + i * sz, buffer, sz);
	return True;
}

static void gzwritebe(gzFile out, APTR buffer, int len)
{
	TEXT   be[8];
	STRPTR s, d;

	for (s = buffer + len - 1, d = be; s >= (STRPTR) buffer; *d ++ = *s --);

	gzwrite(out, be, len);
}

static void NBT_WriteFile(gzFile out, NBT root)
{
	int      type, i;
	NBT      sub;
	STRPTR   p;
	uint16_t len = strlen(root->name);

	gzputc(out, root->type & 0xff);
	gzwritebe(out, &len, 2);
	gzwrite(out, root->name, len);
	switch (root->type & 0xff) {
	case 0: return;
	case 1: gzwrite(out, &root->val.byte, 1); break;
	case 2: gzwritebe(out, &root->val.word,   2); break;
	case 3: gzwritebe(out, &root->val.dword,  4); break;
	case 4: gzwritebe(out, &root->val.qword,  8); break;
	case 5: gzwritebe(out, &root->val.real32, 4); break;
	case 6: gzwritebe(out, &root->val.real64, 8); break;
	case 7: len = strlen(root->val.string);
	        gzwritebe(out, &len, 2);
	        gzwrite(out, root->val.string, root->len); break;
	case 8: len = root->len;
	        gzwritebe(out, &len, 2);
	        gzwrite(out, root->val.string, len); break;
	case 9: type = root->type >> 8;
			gzputc(out, type);
	        gzwritebe(out, &root->len, 4);
	        switch (type) {
	        case 1:
	        case 2:
	        case 3:
	        case 4:
	        case 5:
	        case 6:
				gzwrite(out, root->val.string, root->len * sizeof_type[type]);
				break;
			case 8: // TAG_String
				for (p = root->val.string, i = 0; i < root->len; i ++, p = strchr(p, 0) + 1)
				{
					len = strlen(p);
					gzwritebe(out, &len, 2);
					gzwrite(out, p, len);
				}
				break;
			case 10: // TAG_Compound
				for (i = 0; i < root->len; i ++)
				{
					sub = root->val.array[i];
					for (sub = HEAD(sub->child); sub; NEXT(sub))
						NBT_WriteFile(out, sub);
					gzputc(out, 0); // TAG_End
				}
	        }
	        break;
	case 10:
		for (sub = HEAD(root->child); sub; NEXT(sub))
			NBT_WriteFile(out, sub);
		gzputc(out, 0); // TAG_End
	}
}

void NBT_Save(NBT root, STRPTR path)
{
	gzFile out = gzopen(path, "wb");

	if (out)
	{
		NBT_WriteFile(out, root);
		gzclose(out);
	}
}

void NBT_Dump(NBT root, int level)
{
	static STRPTR tagNames[] = {
		"TAG_End",   "TAG_Byte",   "TAG_Short",     "TAG_Int",    "TAG_Long",
		"TAG_Float", "TAG_Double", "TAG_ByteArray", "TAG_String", "TAG_List",
		"TAG_Compound"
	};
	STRPTR p;
	NBT sub;
	int i, type;

	fprintf(stderr, "%*s", level, "");
	switch (root->type & 0xff) {
	case 0: return;
	case 1: fprintf(stderr, "TAG_Byte(\"%s\"): %d\n", root->name, root->val.byte); break;
	case 2: fprintf(stderr, "TAG_Short(\"%s\"): %d\n", root->name, root->val.word); break;
	case 3: fprintf(stderr, "TAG_Int(\"%s\"): %d\n", root->name, root->val.dword); break;
	case 4: fprintf(stderr, "TAG_Long(\"%s\"): %I64d\n", root->name, root->val.qword); break;
	case 5: fprintf(stderr, "TAG_Float(\"%s\"): %f\n", root->name, root->val.real32); break;
	case 6: fprintf(stderr, "TAG_Double(\"%s\"): %g\n", root->name, root->val.real64); break;
	case 7: fprintf(stderr, "TAG_Byte_Array(\"%s\"): [%d bytes] {", root->name, root->len);
		for (i = 0; i < 10 && i < root->len; i ++)
			fprintf(stderr, "%s%u", i ? ", " : "", root->val.string[i]);
		fprintf(stderr, ", ...}\n");
		break;
	case 8: fprintf(stderr, "TAG_String(\"%s\"): %s\n", root->name, root->val.string); break;
	case 9:
		type = root->type >> 8;
		fprintf(stderr, "TAG_List(\"%s\"): %d entries of type %s\n%*s{\n", root->name, root->len,
			tagNames[type], level, "");
		switch (type) {
		case 1:
		case 2:
		case 3:
		case 4:
		case 5:
		case 6:
			for (i = 0; i < root->len; i ++)
			{
				static STRPTR fmt[] = {0, "%d", "%d", "%d", "%lld", "%g", "%g"};
				TEXT buffer[8];
				fprintf(stderr, "%*s%s: ", level + 3, "", tagNames[type]);
				memcpy(buffer, root->val.string + i * sizeof_type[type], sizeof_type[type]);
				swapbytes(buffer, sizeof_type[type]);
				switch (sizeof_type[type]) {
				case 1: fprintf(stderr, "%d", buffer[0]); break;
				case 2: fprintf(stderr, "%d", * (int16_t *) buffer); break;
				case 4: fprintf(stderr, fmt[type], * (int32_t *) buffer); break;
				case 8: fprintf(stderr, fmt[type], * (int64_t *) buffer); break;
				}
				fputc('\n', stderr);
			}
			break;
		case 9:
		case 7: break;
		case 8:
			for (i = 0, p = root->val.string; i < root->len; i ++, p = strchr(p, 0) + 1)
				fprintf(stderr, "%*sTAG_String: %s\n", level + 3, "", p);
			break;
		case 10:
			for (i = 0; i < root->len; i ++)
				NBT_Dump(root->val.array[i], level + 3);
		}
		fprintf(stderr, "%*s}\n", level, "");
		break;
	case 10:
		fprintf(stderr, "TAG_Compound(\"%s\"): %ld entries\n%*s{\n", root->name, root->child.lh_Count,
			level, "");
		for (sub = HEAD(root->child); sub; NEXT(sub))
			NBT_Dump(sub, level + 3);
		fprintf(stderr, "%*s}\n", level, "");
	}
}

STRPTR NBT_FindItemName(int id)
{
	static TEXT msg[64];
	int i;
	for (i = 0; i < DIM(items); i ++)
		if (items[i].id == id) return items[i].name;

	snprintf(msg, sizeof msg, "Unknown item %d (0x%4x)", id, id);

	return msg;
}

STRPTR NBT_WoolColor(int id)
{
	static STRPTR colors[] = {
		"White", "Orange", "Magenta", "Light Blue", "Yellow", "Light Green",
		"Pink", "Gray", "Light Gray", "Cyan", "Purple", "Blue", "Brown",
		"Dark Green", "Red", "Black"
	};
	if (0 <= id && id <= 15) return colors[id];
	else return colors[0];
}

NBT NBT_Parse(STRPTR path)
{
	ZStream in = gzOpen(path, 0, 0);

	if (in)
	{
		NBT root = NBT_ParseFile(in);
		gzClose(in);
		return root;
	}
	return NULL;
}

/* Get chunkfor region file format of beta 1.3 */
NBT NBT_GetFromRegion(STRPTR path, STRPTR file, int offset)
{
	STRPTR  gz = CatPath(path, "region", file, NULL);
	ZStream in = gzOpen(gz, 1, offset);
	free(gz);

	if (in)
	{
		NBT root = NBT_ParseFile(in);
		gzClose(in);
		return root;
	}
	return NULL;
}
